/**
 * $Id: ScanJarFile.java 4 2010-09-10 17:02:31Z tanktong $
 * Copyright(C) 2010-2014 xiaonei.com/kaixin.com, All Rights Reserved.
 */
package com.tkiicpp.tank.utils.jar;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.tkiicpp.tank.utils.clazz.ClassAnalyzer;
import com.tkiicpp.tank.utils.clazz.constant.ClassConstant;
import com.tkiicpp.tank.utils.clazz.constant.ClassConstant.ConstantType;
import com.tkiicpp.tank.utils.clazz.constant.impl.ClassRefConstant;


/**
 * @author <a href="mailto:kun.tong@opi-corp.com">Tank.Tong</a>
 * @version 1.0 2010-08-19 15:01:07
 * @since 1.0
 */
public class ScanJarFile {

    private static Set<String> multiClass = new HashSet<String>();

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
//        URL url = ScanJarFile.class.getClassLoader().getResource(CommonCst.class.getName().replaceAll("\\.", "/") + ".class");
//        System.out.println(url.getFile());
//        System.out.println(getJarName(url.getFile()));

        try {
            PrintStream p = new PrintStream(new File("c:\\depend.log"));
//            System.setOut(p);
//            System.setErr(p);

            File file = new File("c:/work/workspace/Common_Dependence/workspace/commons/");

            if (!file.exists()) {
                System.out.println("Not exist");
                System.exit(0);
            }

            for (File sub : file.listFiles()) {
                if (sub.isFile() && sub.getName().endsWith(".jar")) {
                    new ScanJarFile().scanJarFile(sub.getAbsolutePath());
                }
            }//"c:/work/workspace/Common_Dependence/workspace/commons/sg-commons-base.jar");
        } catch (Throwable e) {
//            System.out.println(e.getMessage());
            e.printStackTrace();
        }
    }

    private void scanJarFile(String fileName) throws IOException {
        System.out.println(fileName);


        File file = new File(fileName);

        if (!file.exists()) {
            throw new RuntimeException("Cannot find file " + fileName);
        }

        JarFile jar = new JarFile(file);
        Enumeration<JarEntry> entries = jar.entries();

        JarEntry entry = null;
        String entryName = null;
        DepedenceTree tree = new DepedenceTree(file.getName());
        ClassLoader loader = this.getClass().getClassLoader();
//        Node dependence = null;
        try {
        while (entries.hasMoreElements()) {
            entry = entries.nextElement();
            entryName = entry.getName();

            if (!entryName.endsWith(".class")) {
                continue;
            }

            tree.getRoot().addDependence(entryName);
//            dependence = new Node(entryName, jar.getName(), tree);
//            tree.getRoot().addDependence(dependence);

            analyzeClass(loader, entryName, tree);
        }
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("\nAll jar files:");
        printJar(tree);
//        System.out.println("\nTree:\n");
//        printTree(tree);
//        System.out.println("\nDirect dependence jars:");
//        printDirectDependence(tree, file.getName());
        System.out.println("\nJar dependence:");
        printJarDep(tree);
    }

    private void printJarDep(DepedenceTree tree) {
        JarNode node = mergeJarDep(tree);
        System.out.println("Root jar is " + node.jarName);
        for (Entry<JarNode, List<Node>> entry : node.depJars.entrySet()) {
            System.out.println("Depend on jar " + entry.getKey().jarName + " by " + entry.getValue().size() + " classes");
        }
    }

    private JarNode mergeJarDep(DepedenceTree tree) {
        Node root = tree.getRoot();
        Map<String, JarNode> allJars = new HashMap<String, JarNode>();
        JarNode rootJar = new JarNode(root.getJarFile());
        allJars.put(root.jarFile, rootJar);
        JarNode jar = null;
        Node dep = null;
        List<Node> depList = null;
        JarNode depJar = null;

//        System.out.println("Root jar " + root.getJarFile());
        for (Node node : tree.jarNodesMap.get(root.getJarFile())) {
            jar = allJars.get(node.getJarFile());

            if (null == jar) {
                jar = new JarNode(node.getJarFile());
                allJars.put(node.getJarFile(), jar);
            }

            jar.classNode.add(node);

            for (String n : node.dependenceNodes) {
                dep = tree.nodeMap.get(n);
                if (null == dep) {
                    continue;
                }

                if (!dep.getJarFile().equals(node.getJarFile())) {
                    depJar = allJars.get(dep.getJarFile());
                    if (null == depJar) {
                        depJar = new JarNode(dep.getJarFile());
                        allJars.put(dep.jarFile, depJar);
                    }

                    depList = jar.depJars.get(depJar);

                    if (null == depList) {
                        depList = new ArrayList<Node>();
                        jar.depJars.put(depJar, depList);
                    }

                    depList.add(node);
                }
            }
        }

        return rootJar;
    }

    private void printDirectDependence(DepedenceTree tree, String jarName) {
//        System.out.println(jarName);
        List<Node> nodes = tree.jarNodesMap.get(jarName);
        Set<String> jars = new HashSet<String>();
        for (Node node : nodes) {
            for (String dependence : node.getAllDependence()) {
                Node n = tree.getNodeByName(dependence);
                if (null != n) {
                    jars.add(n.getJarFile());
                }
            }
        }

        for (String jar : jars) {
            System.out.println(jar);
        }
    }

    private void printJar(DepedenceTree tree) {
        for (String jarName : tree.jarNodesMap.keySet()) {
            System.out.println(jarName);
        }
    }

    private void printTree(DepedenceTree tree) {
        Node root = tree.getRoot();

        System.out.println("Dependences in jar file " + root.getJarFile());
        Node node = null;
        for (String className : root.getAllDependence()) {
            node = tree.getNodeByName(className);
            System.out.println("\n\nNew line");
            if (null == node) continue;
            printDependence(node, new HashSet<Node>());
        }
    }

    private void printDependence(Node node, Set<Node> printed) {
        System.out.print(" -- " + node.getClassName()); // + "(" + node.getJarFile() + ")");
        printed.add(node);
        Node dependence = null;
        for (String className : node.getAllDependence()) {
            dependence = node.getTree().getNodeByName(className);
            if (printed.contains(dependence)) {
                System.out.print(" -- " + dependence.getClassName()); // + "(" + node.getJarFile() + ") Cycle");
                continue;
            }

            if (null == dependence) continue;
            printDependence(dependence, printed);
        }
    }

    private void analyzeClass(ClassLoader loader, String classFile, DepedenceTree tree) throws IOException {
//        String classFile = node.getClassName();
//        System.out.println("Start to analyze class " + classFile);

        //如果已经解析过，或者正在解析，则返回
        Node node = tree.getNodeByName(classFile);

        if (null != node) {
            return;
        }

        Enumeration<URL> urls = loader.getResources(classFile);

        if (!urls.hasMoreElements()) {
            node = new Node(classFile, "", tree);
            tree.addNode(node);
//            throw new RuntimeException("Cannot find class " + classFile);
            System.err.println("Cannot find class " + classFile);
            return;
        }

        URL url = urls.nextElement();

        boolean isMulti = false;
        StringBuilder s = new StringBuilder();
        while (urls.hasMoreElements()) {
            isMulti = true;
//            System.out.println("Class " + classFile + " has been found from multi jar file :" + urls.nextElement().getPath());
            s.append(getJarName(urls.nextElement().getPath())).append(",");
        }

        if (isMulti) {
            if (!multiClass.contains(classFile)) {
                s.append(getJarName(url.getPath()));
//                System.out.println(classFile + " has been found from (" + s.toString() + ")");
                multiClass.add(classFile);
            }
        }

        String jarName = getJarName(url.getPath());

        if (jarName.equals("rt.jar")) {
            return;
        }

        node = new Node(classFile, jarName, tree);
        tree.addNode(node);

        InputStream input = url.openStream();
        List<ClassConstant> constants = ClassAnalyzer.getClassConstants(input);
        input.close();
        ClassAnalyzer.linkConstants(constants);

        //递归检查当前类的依赖
        ClassRefConstant classRef = null;
        String className = null;
//        Node dependence = null;
        for (ClassConstant constant : constants) {
            if (constant.getType() == ConstantType.CLASS) {
                classRef = (ClassRefConstant) constant;
                className = classRef.getClassName();

                if (className == null || className.trim().length() == 0) {
                    continue;
                }

                if (isSystemClass(className)) {
                    continue;
                }

                if (isCurrentClass(className, classFile)) {
                    continue;
                }

//                dependence = new Node(className, getJarName(url), tree);
                node.addDependence(className + ".class");
                analyzeClass(loader, className + ".class", tree);
            }
        }

//        return node;
    }

    private boolean isSystemClass(String className) {
        return className.startsWith("["); // || className.startsWith("java/")
//                || className.startsWith("javax/") || className.startsWith("sun/")
//                || className.startsWith("(L")|| className.startsWith("com/sun");
    }

    private boolean isCurrentClass(String className, String classFile) {
        return (className + ".class").equals(classFile);
    }

    private static String getJarName(String url) {
        int index = url.indexOf('!');
        url = index == -1 ? url : url.substring(0, index);
        index = url.lastIndexOf('/');
        url = index == -1 ? url : url.substring(index + 1);
        return url;
    }

    private static class DepedenceTree {

        private final Node root;
        private Map<String, Node> nodeMap = new HashMap<String, Node>();
        private Map<String, List<Node>> jarNodesMap = new HashMap<String, List<Node>>();

        public DepedenceTree(String jarName) {
            root = new Node("rootClass", jarName, this);
        }

        public Node getNodeByName(String classFile) {
            return nodeMap.get(classFile);
        }

        public Node getRoot() {
            return root;
        }

        public void addNode(Node node) {
            nodeMap.put(node.getClassName(), node);
            List<Node> nodes = jarNodesMap.get(node.getJarFile());

            if (null == nodes) {
                nodes = new ArrayList<Node>();
                jarNodesMap.put(node.getJarFile(), nodes);
            }

            nodes.add(node);
        }
    }

    private static class Node {

        private DepedenceTree tree;
        private String className;
        private String jarFile;
        private List<String> dependenceNodes = new ArrayList<String>();

        public Node(String className, String jarFile, DepedenceTree tree) {
            this.className = className;
            this.jarFile = jarFile;
            this.tree = tree;
        }

        public void addDependence(String className) {
            this.dependenceNodes.add(className);
        }

        public List<String> getAllDependence() {
            return dependenceNodes;
        }

        public String getClassName() {
            return className;
        }

        public String getJarFile() {
            return jarFile;
        }

        public DepedenceTree getTree() {
            return tree;
        }
    }

    private static class JarNode {

        private String jarName;
        private List<Node> classNode = new ArrayList<Node>();
        private Map<JarNode, List<Node>> depJars = new HashMap<JarNode, List<Node>>();

        public JarNode(String jarName) {
            this.jarName = jarName;
        }

    }
}
